//////////////////////////////////////////////////////////////////////////////////
//
// ActionScript 3.0 Common Library (MIT License)
// Copyright (c) 2009 AS3CommonCode | Tony Birleffi | http://www.mosaiceye.com
// 
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
//////////////////////////////////////////////////////////////////////////////////

package org.as3commoncode.utils
{
	import flash.display.DisplayObjectContainer;	

	/**
	 * This is a utility class with many useful methods for arrays.
	 * 
	 * @author Tony Birleffi
	 * 
	 * TODO: Add help examples.
	 */
	public class ArrayUtil 
	{
		/**
		 * Search and return the index for specific object in an array.
		 * 
		 * @param array		the array upon which to work.
		 * @param search	the object value we are searching for.
		 * @return Number.
		 */
		public static function search(array:Array, search:Object):Number
		{
			var len:Number = array.length;
			for(var i:Number = len; i > -1; --i) if(array[ i ] == search) return i;
			return 0;
		}
		
		/**
		 * Get a random element from the Array.
		 * 
		 * @param array		the array upon which to work.
		 * @return Object.
		 */
		public static function randomItem(array:Array):Object 
		{
			var len:Number = array.length;
			if (len > 0) return array[ Math.floor(Math.random() * array.length) ];	
			return null;
		}
		
		/**
		 * Get a random index from the Array.
		 * 
		 * @param array		the array upon which to work.
		 * @return Number.
		 */
		public static function randomIndex(array:Array):Number
		{
			var len:Number = array.length;
			if (len > 0) return Math.floor(Math.random() * array.length);	
			return 0;
		}
		
		/**
		 * Shuffles the passed-in array.
		 * 
		 * @param array		the array to shuffle.
		 */
		public static function shuffle(array:Array):void
		{
			var len:Number = array.length; 
			var rand:Number;
			var temp:Object;
			
			for (var i:Number = len-1; i >= 0; i--)
			{ 
				rand = Math.floor(Math.random() * len); 
				temp = array[ i ]; 
				array[ i ] = array[ rand ]; 
				array[ rand ] = temp; 
			}
		}
		
		/**
		 * Check to make sure the index falls within the bounds of the array.
		 * If its greater than the length reset it to the first index, if its less than 0 reset it to the end index. 
		 * If its within the bounds just send the index back.
		 * 
		 * @param array		the array upon which to work.
		 * @param index		the current index to check.
		 * @return Number.
		 */
		public static function checkBounds(array:Array, index:Number):Number
		{
			var len:Number = array.length;
			if(index < 0) index = len - 1;
			else if(index > len - 1) index = 0;
			else index = index;
			return index;
		}
		
		/**
		 * Have all of the elements in the array executed with an anonymous function.
		 * The example will accomplish the same thing as the example for the walk array method.
		 * 
		 * @param array 			the array upon which to work.
		 * @param functionToCall 	the function to call on each object.
		 */
		public static function callFunction(array:Array, functionToCall:String):void
		{
			var total:Number = array.length;
			for(var i:Number = 0; i < total; i++) if (array[ i ][ functionToCall ] != null) array[ i ][ functionToCall ]();
		}
		
		/**
		 * Concatinates the properties of one array to another. 
		 * If you wish to only overwrite a small set of properties you can set vOverwrite to true and list all of the properties you do not want to overwrite in vSkip. 
		 * Conversly, if it is only two or three properties you want to set just set them item by item without using this function. 
		 * As this spits out an object, you will have to cast your return as an array if you wish to concatinate arrays.
		 *
		 * @param	master			the object to which you wish to add properties.
		 * @param	slave 			the object from which you wish to transfer properties to master.
		 * @param	overwrite		[Optional] true = overwrite properties on the master if they exist on the slave // false (default) = no not overwrite properties on the master.
		 * @param	skip 			[Optional] list of properties as strings to ignore (ie. will not be added from slave to master).
		 * @return	Array.
		 */
		public static function concat(master:Array, slave:Array, overwrite:Boolean = false, skip:Array = null):Array
		{
			// Loop through slave properties adding them to the master.
			for (var i:String in slave)
			{
				// Declare variables.
				var totalSkip:Number = skip.length;
				var skipItem:Boolean = false;
				
				// Check for skip properties.
				for(var j:Number = 0; j < totalSkip; j++)
				{
					// If the current property is one of the properties to skip...
					if (skip[ j ] == i)
					{
						// Set the skip boolean.
						skipItem = true;
						
						// Stop this loop.
						break;
					}
				}
				// If we need to skip this property...
				if (skipItem)
					// Continue to the next iteration of the loop so that the property is not set.
					continue;
				
				// If the property exists already and we do not want to overwrite it.
				if (!overwrite)
					// Continue to the next iteration of the loop so that the property is not set.
					continue;
				
				// If this property is an object/array but not a movieclip...
				if ((slave[ i ] is Object || slave[ i ] is Array) && !(slave[ i ] is DisplayObjectContainer))
				{
					// Add the empty object/array from which to start.
					master[ i ] = (slave[ i ] is Array) ? [] : {};
					
					// Recursively copy all of its properties so that we don't have any pointers.
					master[ i ] = ObjectUtil.concat(master[ i ], slave[ i ], overwrite, skip);
				}
				else
					// Add from slave to master.
					master[ i ] = slave[ i ];
			}
			return master;
		}
		
		/**
		 * Return a copy of the array. 
		 * This is useful incase the object contains properties which are arrays. 
		 * In this case, not using this function would simply add that property as a reference to the array property on the other object. 
		 * Thus, changes to the array property's data in either object would change the array property's data in the other object.
		 *
		 * @param object		the object upon which to work.
		 * @return Array.
		 */
		public static function clone(object:Array):Array
		{
			// Make sure nothing other than objects/arrays were passed because we are using untyped
			// function parameters and an untyped function return.
			if (object is String || object is Number || object is Boolean)
				// Stop
				return object;
			
			// Declare variables.
			var temp:Array = [];
			
			// Concat all data from the object passed, into an empty object.
			return ArrayUtil.concat(temp, object);
		}
		
		/**
		 * Compare two arrays to see if they are identical. 
		 * They must be sorted the same way to return true.
		 * This is a recursive function so all nested objects/arrays will also be tested.
		 *
		 * @param array		the array upon which to work.
		 * @param slave		array we're comparing to.
		 * @return Boolean.
		 */
		public static function identical(master:Array, slave:Array):Boolean
		{
			// If a is a reference to master... they match.
			if (master == slave) return true;
			
			// If 2 arrays have different lengths... they don't match.
			var i:Number = master.length;	
			if (i != slave.length) return false;
			
			// Test to see that all of the properties inside this array match.
			return matchArrayProperties(master, slave);
		}
		
		/**
		 * Checks all of the elements in an array to make sure that all of them match.
		 * 
		 * @param array		the array upon which to work.
		 * @param slave		array we're comparing to.
		 * @return Boolean.
		 */
		private static function matchArrayProperties(master:Array, slave:Array):Boolean
		{
			// Declare variables.
			var masterCopy:Number = 0;
			
			// For all of the properties in the master, check the slave.
			for(var i:String in master)
			{
				// If the properties in the master do not match those of the slave... return false.
				if (Object(master[ i ]) == null && master[ i ] != slave[ i ]) return false;
				
				// Otherwise, if they match
				else
				{
					// If the current property is an array or an object.
					if (Object(master[ i ]))
					{
						// Recursively match that object's properties.
						if (!matchArrayProperties(master[ i ], slave[ i ]))
							// They do not match.
							return false;
					}
				}
				// Update the count.
				++masterCopy;
			}
			
			// If the master has more properties than the slave.
			if (masterCopy != slave.length)
				// They do not match.
				return false;
			
			// Otherwise, everything matches
			return true;
		}
		
		/**
		 * Return elements that do not exist in both arrays.
		 * This function should be depricated by adding this functionality into sameArray.
		 * 
		 * @param array				the array upon which to work.
		 * @param compareArray		array we're comparing to.
		 * @return Array.
		 */
		public static function diffArray(array:Array, vPool:Array):Array 
		{
			// declare variables.
			var tempArray:Array = [];
			var arrayLength:Number = array.length;
			var compareArrayLength:Number = vPool.length;
			
			// outer 1 loop
			for (var i:Number = 0; i < arrayLength; i++)
			{			
				// inner 1 loop
				for (var j:Number = 0; j < compareArrayLength; j++) if(array[ i ] == vPool[ j ]) break;
				if (j == compareArrayLength) tempArray.push(array[ i ]);
			}
			
			// outer 2 loop.
			for (var k:Number = 0; k < compareArrayLength; k++)
			{		
				// inner inner 2 loop
				for (var l:Number = 0; l < arrayLength; l++) if(vPool[ k ] == array[ l ]) break;
				if(l == arrayLength) tempArray.push(vPool[ k ]);
			}
			return tempArray;
		}
		
		/**
		 * Return elements that exist in both arrays.
		 * 
		 * @param array			the array upon which to work.
		 * @param compareArray 	array we're comparing to.
		 * @return Array.
		 */
		public static function sameArray(array:Array, compareArray:Array):Array 
		{
			// Declare variables.
			var tempArray:Array = [];
			var contains:Boolean = false;
			var arrayLength:Number = array.length;
			var compareArrayLength:Number = compareArray.length;
			
			// Mega loop.
			for(var i:Number = 0; i < arrayLength; i++)
			{
				for(var j:Number = 0; j < compareArrayLength; j++)
				{
					if(array[ i ] == compareArray[ j ])
					{
						for(var k:Number = 0; k < tempArray.length; k++)
						{
							if(array[ i ] == tempArray[ k ]) contains = true;
						}
						
						if(contains == false) tempArray.push(array[ i ]);
						contains = false;
					}
				}
			}
			
			for(var l:Number = 0; l < compareArrayLength; l++)
			{
				for(var m:Number = 0; m < arrayLength; m++)
				{
					if (compareArray[ l ] == array[ m ])
					{
						for(var n:Number = 0; n < tempArray.length; n++)
						{
							if(compareArray[ l ] == tempArray[ n ]) contains = true;
						}
						
						if(contains == false) tempArray.push(compareArray [ l ]); contains = false;
					}
				}
			}
			return tempArray;
		}
		
		/**
		 * Searches the elements within an array for objects/arrays with the 
		 * property name and optionally a specific value for that property.
		 * 
		 * @param array		the array to search through.
		 * @param name		the name of the property we wish to search for.
		 * @param value	[Optional] a specific value for that property.
		 * @return Array.
		 */
		public static function findByProperty(array:Array, name:String, value:String):Array
		{
			// Declare variables.
			var tempArray:Array = [];
			var total:Number = array.length;
			
			// Loop through the indexed properties of this array searching the inner arrays.
			for(var i:Number = 0; i < total; i++)
			{
				// Declare variables
				var current:String = array[ i ];
				
				// If the current object has the matching property.
				if (current[ name ] != null)
				{
					// If a specific value must also be matched.
					if (value != null)
					{
						// If the value passed matches the value of the property.
						if (current[ name ] == value)
							// Add this object to the array of matches.
							tempArray.push(current);
					}
					else
						// Simply add this object to the array of matches.
						tempArray.push(current);
				}
			}
			// Return the array of matches.
			return tempArray;
		}
		
		/**
		 * Returns the value of the property specified for each object in this list. 
		 * For example, if the list contains objects with the property "x", a list is returned with all of the "x" values in the order they originally existed.
		 * If any of the items in the list are not objects, the function will throw an error.
		 *
		 * @param data			the array in which to look.
		 * @param property		the name of the property we are looking for.
		 * @param matchAll		[Optional] true = if the property doesn't exist on the object, leave an empty space in the list. false = (default) if the property doesn't exist, trace an error.
		 * @return Array.
		 */
		public static function getProperties(data:Array, property:String, matchAll:Boolean = false):Array
		{
			// Declare variables.
			var total:Number = data.length;
			var tempArray:Array = [];
			
			// Search through the data object looking for this property.
			for(var i:Number = 0; i < total; i++)
			{
				// Declare variables.
				var current:String = data[ i ][ property ];
				
				// If the property exists.
				if (current != null) tempArray.push(current);
				else if(matchAll) tempArray[ i ] = null;
				else
				{
					//trace("Property Missing: The getProperties method attempted to find the property " + property + " on the array " + data + " but was unable to find it as position " + i + ".");
					return null;
				}
			}
			// Return the new array
			return tempArray;
		}
		
		/**
		 * Determines whether the specified array contains the specified value.
		 * 
		 * @param array		the array that will be checked for the specified value.
		 * @param value		the object which will be searched for within the array.
		 * @return Boolean.
		 */			
		public static function containsValue(array:Array, value:Object):Boolean
		{
			return (array.indexOf(value) != -1);
		}
		
		/**
		 * Remove all instances of the specified value from the array.
		 * 
		 * @param array		the array from which the value will be removed.
		 * @param value		the object that will be removed from the array.
		 */		
		public static function removeValue(array:Array, value:Object):void
		{
			var len:Number = array.length;
			for(var i:Number = len; i > -1; i--) if(array[ i ] === value) array.splice(i, 1);				
		}
		
		/**
		 * Switch element positions within the array.
		 * 
		 * @param array			array to work on.
		 * @param indexA		first element to switch.
		 * @param indexB		second element to switch.
		 */
		public static function switchElements(array:Array, indexA:Number, indexB:Number):void
		{
			var elementA:Object = array[ indexA ];
			var elementB:Object = array[ indexB ];
			array.splice(indexA, 1, elementB);
			array.splice(indexB, 1, elementA);
	    }
	    
	    /**
	     * This remove's duplicates in an array.
	     * 
	     * @param array		Array.
	     */
	    public static function removeDuplicates(array:Array):Array
		{
			for(var i:uint = 0; i < array.length; i++)
			{
				for(var j:uint = 0; j < array.length; j++) 
				{
					if(i != j) if(array[ i ] == array[ j ]) array.splice(j, 1);
				}
			}
			return array;
		}
		
		/**
		 * Returns a paginated array of values.
		 * 
		 * @param total			int.
		 * @param maxPerPage	int.
		 * 
		 * @return Array.
		 */
		public static function paginate(total:int, maxPerPage:int = 5):Array
		{
			// Define.
			var numOfSections:int = Math.round(total / maxPerPage);
			var arr:Array = [];
			
			// Calculate.
			if(numOfSections == 0) for(var i:int = 0; i < total; i++) arr.push(i + 1);
			else
			{
				for(var j:int = 0; j < numOfSections; j++)
				{
					var nums:Array = [];
					var diff:int = (j * maxPerPage);
					for(var f:int = diff; f < (diff + maxPerPage); f++) 
					{
						var num:int = (f + 1);
						if(f < total) nums.push(num);
					}
					arr.push(nums);
				}
			}
			
			// Return the calculated array.
			return arr;
		}
	}
}
